package com.dddframework.security.api;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.dddframework.core.context.SpringContext;
import com.dddframework.core.context.ThreadContext;
import com.dddframework.core.contract.R;
import com.dddframework.core.contract.constant.ContextConstants;
import com.dddframework.kit.lang.MapKit;
import com.dddframework.kit.lang.StrKit;
import com.dddframework.security.domain.auth.model.AuthUser;
import com.dddframework.security.domain.contract.constants.CacheConstants;
import com.dddframework.security.domain.contract.event.UserCloseEvent;
import com.dddframework.web.auth.annotation.Inside;
import lombok.AllArgsConstructor;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.ConvertingCursor;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * token api
 *
 * @author zhouzx
 */
@RestController
@AllArgsConstructor
@RequestMapping("/token")
public class TokenController {
    private static final String BASE_OAUTH_ACCESS = "base_oauth_auth_to_access:*";
    private final ClientDetailsService clientDetailsService;
    private final TokenStore tokenStore;
    private final RedisTemplate redisTemplate;
    private final CacheManager cacheManager;

    /**
     * 认证页面
     *
     * @return ModelAndView
     */
    @GetMapping("/login")
    public ModelAndView require(ModelAndView modelAndView) {
        modelAndView.setViewName("ftl/login");
        return modelAndView;
    }

    /**
     * 确认授权页面
     */
    @GetMapping("/confirm_access")
    public ModelAndView confirm(HttpServletRequest request, HttpSession session, ModelAndView modelAndView) {
        Map<String, Object> scopeList = (Map<String, Object>) request.getAttribute("scopes");
        modelAndView.addObject("scopeList", scopeList.keySet());

        Object auth = session.getAttribute("authorizationRequest");
        if (auth != null) {
            AuthorizationRequest authorizationRequest = (AuthorizationRequest) auth;
            ClientDetails clientDetails = clientDetailsService.loadClientByClientId(authorizationRequest.getClientId());
            modelAndView.addObject("app", clientDetails.getAdditionalInformation());
            modelAndView.addObject("user", AuthUser.of());
        }

        modelAndView.setViewName("ftl/confirm");
        return modelAndView;
    }

    /**
     * 退出token
     *
     * @param authorization Authorization
     */
    @DeleteMapping("/logout")
    public R logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorization) {
        if (StrKit.isBlank(authorization)) {
            return R.ok("退出失败，token 为空", false);
        }
        String token = authorization.replace(OAuth2AccessToken.BEARER_TYPE, StrKit.EMPTY).trim();
        OAuth2AccessToken accessToken = tokenStore.readAccessToken(token);
        if (accessToken == null || StrKit.isBlank(accessToken.getValue())) {
            return R.ok("退出失败，token 无效", true);
        }
        OAuth2Authentication auth2Authentication = tokenStore.readAuthentication(accessToken);
        // 清空用户信息
        cacheManager.getCache(CacheConstants.USER_CACHE).evict(auth2Authentication.getName());
        // 清空access token
        tokenStore.removeAccessToken(accessToken);
        // 清空 refresh token
        OAuth2RefreshToken refreshToken = accessToken.getRefreshToken();
        tokenStore.removeRefreshToken(refreshToken);
        return R.ok(true);
    }

    /**
     * 删除令牌
     *
     * @param token token
     */
    @Inside
    @DeleteMapping("/{token}")
    public R<Boolean> removeToken(@PathVariable("token") String token) {
        OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(token);
        tokenStore.removeAccessToken(oAuth2AccessToken);
        return R.ok(true);
    }


    /**
     * 查询token
     *
     * @param params 分页参数
     */
    @Inside
    @PostMapping("/page")
    public R<Page> tokenList(@RequestBody Map<String, Object> params) {
        //根据分页参数获取对应数据
        String key = String.format("%s*:%s", BASE_OAUTH_ACCESS, ThreadContext.get(ContextConstants.TENANT_ID));
        List<String> pages = findKeysForPage(key, MapKit.getInt(params, "current"), MapKit.getInt(params, "size"));
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        Page result = new Page(MapKit.getInt(params, "current"), MapKit.getInt(params, "size"));
        result.setRecords(redisTemplate.opsForValue().multiGet(pages));
        result.setTotal(redisTemplate.keys(key).size());
        return R.ok(result);
    }


    private List<String> findKeysForPage(String patternKey, int pageNum, int pageSize) {
        ScanOptions options = ScanOptions.scanOptions().match(patternKey).build();
        RedisSerializer<String> redisSerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer();
        Cursor cursor = (Cursor) redisTemplate.executeWithStickyConnection(
                redisConnection -> new ConvertingCursor<>(redisConnection.scan(options), redisSerializer::deserialize));
        List<String> result = new ArrayList<>();
        int tmpIndex = 0;
        int startIndex = (pageNum - 1) * pageSize;
        int end = pageNum * pageSize;

        assert cursor != null;
        while (cursor.hasNext()) {
            if (tmpIndex >= startIndex && tmpIndex < end) {
                result.add(cursor.next().toString());
                tmpIndex++;
                continue;
            }
            if (tmpIndex >= end) {
                break;
            }
            tmpIndex++;
            cursor.next();
        }
        return result;
    }

    @PostMapping("/close")
    public R close(@RequestHeader(value = HttpHeaders.AUTHORIZATION) String authorization) {
        String token = authorization.replace(OAuth2AccessToken.BEARER_TYPE, StrKit.EMPTY).trim();
        OAuth2AccessToken accessToken = tokenStore.readAccessToken(token);
        OAuth2Authentication auth2Authentication = tokenStore.readAuthentication(accessToken);
        AuthUser authUser = AuthUser.of(auth2Authentication);
        // 清空用户信息
        cacheManager.getCache(CacheConstants.USER_CACHE).evict(auth2Authentication.getName());
        // 清空access token
        tokenStore.removeAccessToken(accessToken);
        // 清空 refresh token
        OAuth2RefreshToken refreshToken = accessToken.getRefreshToken();
        tokenStore.removeRefreshToken(refreshToken);
        SpringContext.getApplicationContext().publishEvent(new UserCloseEvent(authUser));
        return R.ok(true);
    }
}